﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Fetches.Process.Html2Entity.Attributes;

namespace Fetches.Process.Html2Entity
{
    public static class HtmlEntityConverter
    {
        private static ConcurrentDictionary<Type, object> CacheInfo = new ConcurrentDictionary<Type, object>();

        private static ConcurrentDictionary<Type, List<KeyValuePair<string, HtmlNodeAttribute>>> CacheRowsInfo =
            new ConcurrentDictionary<Type, List<KeyValuePair<string, HtmlNodeAttribute>>>();

        #region Entity

        internal static Dictionary<string, Action<T, object>> GetSetter<T>() where T : class, new()
        {
            var type = typeof(T);
            if (CacheInfo.TryGetValue(type, out var result))
            {
                if (result is Dictionary<string, Action<T, object>>)
                    return result as Dictionary<string, Action<T, object>>;
            }

            var list = new Dictionary<string, Action<T, object>>();
            var propertyInfos = type.GetProperties();
            foreach (var property in propertyInfos)
            {
                if (!property.CanWrite) continue;
                ParameterExpression parameter = Expression.Parameter(type, property.Name);
                ParameterExpression value = Expression.Parameter(typeof(object), property.Name);
                MethodInfo setter = type.GetMethod("set_" + property.Name);
                MethodCallExpression call =
                    Expression.Call(parameter, setter, Expression.Convert(value, property.PropertyType));
                var lambda = Expression.Lambda<Action<T, object>>(call, parameter, value);
                var action = lambda.Compile();
                list.Add(property.Name, action);
            }

            CacheInfo[type] = list;
            return list;
        }

        internal static List<KeyValuePair<string, HtmlNodeAttribute>> GetNodesAttributes<T>() where T : class, new()
        {
            var type = typeof(T);
            if (CacheRowsInfo.TryGetValue(type, out var result))
            {
                if (result is List<KeyValuePair<string, HtmlNodeAttribute>>)
                    return result as List<KeyValuePair<string, HtmlNodeAttribute>>;
            }

            var attrList = AttributeHelper.GetNodesAttributes<T>();
            if (attrList != null && attrList.Count > 0)
                CacheRowsInfo[type] = attrList;
            return attrList;
        }

        public static List<T> ToList<T>(this string htmlContent) where T : class, new()
        {
            List<T> list = new List<T>();
            if (String.IsNullOrEmpty(htmlContent))
            {
                return list;
            }

            var setterList = GetSetter<T>();
            var attrList = GetNodesAttributes<T>();
            var type = typeof(T);
            var propertyInfos = type.GetProperties();
            var parsedDoc = ParseAllSuitable<T>(htmlContent, out var success);
            if (!success)
                return list;

            foreach (var doc in parsedDoc)
            {
                T t = new T();
                foreach (var kvp in doc)
                {
                    var property = propertyInfos.FirstOrDefault(m => m.Name == kvp.Key);
                    if (property == null)
                        continue;
                    var attr = attrList.FirstOrDefault(m => m.Key == kvp.Key);
                    if (attr.Value == null | attr.Value.Ignore)
                        continue;
                    var setter = setterList.FirstOrDefault(m => m.Key == kvp.Key);
                    if (setter.Value == null)
                        continue;
                    if (kvp.Value == null || kvp.Value.Count < 1)
                        continue;
                    SetValue(t, kvp, setter, property);
                }

                list.Add(t);
            }

            return list;
        }

        public static T ToEntity<T>(this string htmlContent) where T : class, new()
        {
            if (String.IsNullOrEmpty(htmlContent))
                return default(T);

            var setterList = GetSetter<T>();
            var attrList = GetNodesAttributes<T>();
            var type = typeof(T);
            var propertyInfos = type.GetProperties();
            var parsedDoc = ParseFirstSuit<T>(htmlContent, out var success);
            if (!success)
                return default(T);
            T t = new T();
            foreach (var kvp in parsedDoc)
            {
                var property = propertyInfos.FirstOrDefault(m => m.Name == kvp.Key);
                if (property == null)
                    continue;
                var attr = attrList.FirstOrDefault(m => m.Key == kvp.Key);
                if (attr.Value == null | attr.Value.Ignore)
                    continue;
                var setter = setterList.FirstOrDefault(m => m.Key == kvp.Key);
                if (setter.Value == null)
                    continue;
                if (kvp.Value == null || kvp.Value.Count < 1)
                    continue;
                SetValue(t, kvp, setter, property);
            }

            return t;
        }

        private static void SetValue<T>(T t, KeyValuePair<string, List<string>> kvp,
            KeyValuePair<string, Action<T, object>> setter, PropertyInfo property) where T : class, new()
        {
            string value = null;

            if (kvp.Value.Count == 1)
            {
                value = kvp.Value[0];
            }
            else
            {
                if (property.PropertyType == typeof(string))
                    value = String.Join(",", kvp.Value);
                else
                    value = kvp.Value[0];
            }

            var nullable = IsNullableType(property.PropertyType);
            if (!nullable)
            {
                if (value == null)
                    throw new Exception($"{nameof(property.Name)} 不能为空");

                object safeValue = Convert.ChangeType(value, property.PropertyType);
                setter.Value(t, safeValue);
            }
            else
            {
                if (!String.IsNullOrEmpty(value.ToString()))
                {
                    object safeValue = Convert.ChangeType(value, property.PropertyType);
                    setter.Value(t, safeValue);
                }
            }
        }

        internal static bool IsNullableType(Type theType)
        {
            return (theType.IsGenericType && theType.GetGenericTypeDefinition().Equals
                        (typeof(Nullable<>)));
        }


        #endregion

        #region HTML

        internal static Dictionary<string, List<string>> ParseFirstSuit<T>(string htmlContent, out bool success) where T : class, new()
        {
            success = false;
            Dictionary<string, List<string>> dic = new Dictionary<string, List<string>>();
            var rootAttr = AttributeHelper.GetRootAttribute<T>();
            if (String.IsNullOrEmpty(rootAttr.Selector))
                return dic;
            var attrList = HtmlEntityConverter.GetNodesAttributes<T>();
            try
            {
                var document = new HtmlParser().ParseDocument(htmlContent);
                if (document.ChildElementCount < 1)
                    return dic;
                IElement root = GetRootElement(rootAttr, document);
                if (root == null)
                    return dic;
                foreach (var attr in attrList)
                {
                    if (attr.Value.Ignore)
                        continue;
                    if (String.IsNullOrEmpty(attr.Value.Selector))
                        continue;
                    var list = GetElementValue(attr.Value, root);
                    dic.Add(attr.Key, list);
                }
                success = true;
            }
            catch (Exception e)
            {
                throw e;
            }
            return dic;
        }

        internal static List<Dictionary<string, List<string>>> ParseAllSuitable<T>(string htmlContent, out bool success) where T : class, new()
        {
            success = false;
            List<Dictionary<string, List<string>>> list = new List<Dictionary<string, List<string>>>();
            success = false;
            var rootAttr = AttributeHelper.GetRootAttribute<T>();
            if (String.IsNullOrEmpty(rootAttr.Selector))
                return list;
            var attrList = HtmlEntityConverter.GetNodesAttributes<T>();
            try
            {
                var document = new HtmlParser().ParseDocument(htmlContent);
                if (document.ChildElementCount < 1)
                    return list;
                if (rootAttr.IsSingle)
                {
                    IElement root = GetRootElement(rootAttr, document);
                    if (root == null)
                        return list;
                    var dic = new Dictionary<string, List<string>>();
                    foreach (var attr in attrList)
                    {
                        if (attr.Value.Ignore)
                            continue;
                        if (String.IsNullOrEmpty(attr.Value.Selector))
                            continue;
                        var values = GetElementValue(attr.Value, root);
                        if (values != null && values.Count > 0)
                        {
                            dic.Add(attr.Key, values);
                        }
                    }
                    list.Add(dic);
                }
                else
                {
                    var roots = GetRootElements(rootAttr, document);
                    if (roots == null || roots.Length < 1)
                        return list;
                    foreach (var root in roots)
                    {
                        var dic = new Dictionary<string, List<string>>();
                        foreach (var attr in attrList)
                        {
                            if (attr.Value.Ignore)
                                continue;
                            if (String.IsNullOrEmpty(attr.Value.Selector))
                                continue;
                            var values = GetElementValue(attr.Value, root);
                            if (values != null && values.Count > 0)
                            {
                                dic.Add(attr.Key, values);
                            }
                        }
                        list.Add(dic);
                    }
                }
                success = true;
            }
            catch (Exception e)
            {
                throw e;
            }
            return list;
        }

        private static IElement GetRootElement(HtmlNodeAttribute attribute, IHtmlDocument document)
        {
            IElement e = null;

            if (attribute.Index == 1)
            {
                e = document.QuerySelector(attribute.Selector);
            }
            else
            {
                var ec = document.QuerySelectorAll(attribute.Selector);
                if (ec.Length < attribute.Index)
                    return null;
                return ec[attribute.Index - 1];
            }
            return e;
        }

        private static IHtmlCollection<IElement> GetRootElements(HtmlNodeAttribute attribute, IHtmlDocument document)
        {
            IHtmlCollection<IElement> elements = document.QuerySelectorAll(attribute.Selector);
            return elements;
        }

        private static List<string> GetElementValue(HtmlNodeAttribute attribute, IElement element)
        {
            List<string> valueList = new List<string>();
            if (attribute.IsSingle)
            {
                if (attribute.Index == 1)
                {
                    var e = element.QuerySelector(attribute.Selector);
                    if (e == null)
                        return valueList;
                    valueList = GetElementInnerValue(attribute, e);
                }
                else
                {
                    var e = element.QuerySelectorAll(attribute.Selector);
                    if (e.Length < attribute.Index)
                        return valueList;
                    valueList = GetElementInnerValue(attribute, e[attribute.Index - 1]);
                }
            }
            else
            {
                var elements = element.QuerySelectorAll(attribute.Selector);
                if (elements != null && elements.Length > 0)
                    valueList = GetElementInnerValue(attribute, elements);
            }
            return valueList;
        }

        private static List<string> GetElementInnerValue(HtmlNodeAttribute attribute, IElement element)
        {
            List<string> valueList = new List<string>();
            string value = null;
            switch (attribute.ValueFrom)
            {
                case HtmlNodeValue.Html:
                    value = element.InnerHtml;
                    break;
                case HtmlNodeValue.Content:
                    value = element.TextContent;
                    break;
                case HtmlNodeValue.Attribute:
                    if (!String.IsNullOrEmpty(attribute.AttributeValue) && element.HasAttribute(attribute.AttributeValue))
                        value = element.GetAttribute(attribute.AttributeValue);
                    break;
            }
            if (!String.IsNullOrEmpty(value) && !String.IsNullOrEmpty(attribute.TrimCharacter))
            {
                var splist = attribute.TrimCharacter.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                foreach (var sp in splist)
                    value = value.Replace(sp, "");
            }
            if (!String.IsNullOrEmpty(value))
                valueList.Add(value.Trim());
            return valueList;
        }

        private static List<string> GetElementInnerValue(HtmlNodeAttribute attribute, IHtmlCollection<IElement> elements)
        {
            List<string> valueList = new List<string>();
            foreach (var element in elements)
            {
                string value = null;
                switch (attribute.ValueFrom)
                {
                    case HtmlNodeValue.Html:
                        value = element.InnerHtml;
                        break;
                    case HtmlNodeValue.Content:
                        value = element.TextContent;
                        break;
                    case HtmlNodeValue.Attribute:
                        if (!String.IsNullOrEmpty(attribute.AttributeValue) && element.HasAttribute(attribute.AttributeValue))
                            value = element.GetAttribute(attribute.AttributeValue);
                        break;
                }
                if (!String.IsNullOrEmpty(value) && !String.IsNullOrEmpty(attribute.TrimCharacter))
                {
                    var splist = attribute.TrimCharacter.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (var sp in splist)
                        value = value.Replace(sp, "");
                }
                if (!String.IsNullOrEmpty(value))
                    valueList.Add(value.Trim());
            }
            return valueList;
        }

        #endregion
    }
}